package itecs;

//import java.io.FileWriter;
//import java.io.IOException;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.LinkedList;
import java.util.List;

import models.SLA;

import org.cloudbus.cloudsim.Cloudlet;
import org.cloudbus.cloudsim.CloudletSchedulerSpaceShared;
import org.cloudbus.cloudsim.Datacenter;
import org.cloudbus.cloudsim.DatacenterCharacteristics;
import org.cloudbus.cloudsim.Host;
import org.cloudbus.cloudsim.Log;
import org.cloudbus.cloudsim.Pe;
import org.cloudbus.cloudsim.Storage;
import org.cloudbus.cloudsim.UtilizationModelFull;
import org.cloudbus.cloudsim.Vm;
import org.cloudbus.cloudsim.VmAllocationPolicySimple;
import org.cloudbus.cloudsim.VmSchedulerSpaceShared;
import org.cloudbus.cloudsim.core.CloudSim;
import org.cloudbus.cloudsim.provisioners.BwProvisionerSimple;
import org.cloudbus.cloudsim.provisioners.PeProvisionerSimple;
import org.cloudbus.cloudsim.provisioners.RamProvisionerSimple;

import simulation.Observation;
import util.Globals;
import util.MathUtils;
import models.ExtendedCloudlet;
import brokers.FCFSBroker;
import brokers.HOLBroker;
import brokers.PDPBroker;
import brokers.SkeletonBroker;



/**
 * 
 * Initialized the cloud and starts the simulations
 * 
 */
public class Itecs {

	private SkeletonBroker broker;
	private ArrayList<ExtendedCloudlet> cloudletList = new ArrayList<ExtendedCloudlet>();
	private UtilizationModelFull utilizationModel;
	private ArrayList<Vm> vmList = new ArrayList<Vm>();
	private boolean SLAsIsMet = false;
	private int m = 0;
	private String brokerType;
	private String serviceDistribution;
	private SLA sla1;
	private SLA sla2;
	private long seed=12573;
	private double lambda1;
	private double lambda2;
	private static double mu;
	//protected static FileWriter writer;


	public Itecs(String brokerType, SLA sla1, SLA sla2, String serviceDistribution, double lambda1, double lambda2) throws Exception{

		this.brokerType = brokerType;
		this.sla1 = sla1;
		this.sla2 = sla2;
		this.serviceDistribution = serviceDistribution;
		this.lambda1 = lambda1;
		this.lambda2 = lambda2;

	}





	private ArrayList<Vm> getVms(int brokerId, int vmsNumber) {

		ArrayList<Vm> list = new ArrayList<Vm>(); 

		for (int i=0; i<vmsNumber; i++){
			Vm vm = new Vm(i, brokerId, Globals.VM_MIPS, Globals.VM_CPU_NUMBER, Globals.VM_RAM, Globals.VM_BW, Globals.VM_SIZE, Globals.VM_NAME + "-" + i, new CloudletSchedulerSpaceShared());
			list.add(vm);
		}

		return list;
	}



	private ArrayList<ExtendedCloudlet> getCloudlets(ArrayList<Double> arrivalTimes1, ArrayList<Double> arrivalTimes2, ArrayList<Double> serviceTimes1, ArrayList<Double> serviceTimes2) {

		ArrayList<ExtendedCloudlet> list = new ArrayList<ExtendedCloudlet>();
		ExtendedCloudlet c;
		utilizationModel=new UtilizationModelFull();
		int id = 0;

		for (int i = 0; i < arrivalTimes1.size(); i++){
			c = createCloudlet(id++, arrivalTimes1.get(i), serviceTimes1.get(i).longValue(), 1);
			list.add(c);
		}
		for (int i = 0; i < arrivalTimes2.size(); i++){
			c = createCloudlet(arrivalTimes1.size() + i, arrivalTimes2.get(i), serviceTimes2.get(i).longValue(), 2);
			list.add(c);
		}

		return list;
	}


	private ExtendedCloudlet createCloudlet(int id, Double arrivalTime, long serviceTime, int classId) {

		long cloudletLength =  serviceTime;

		ExtendedCloudlet c = new ExtendedCloudlet(id, cloudletLength, Globals.CPU_PER_CLOUDLET, Globals.CLOUDLET_FILE_SIZE, Globals.CLOUDLET_OUTPUT_SIZE, utilizationModel, utilizationModel, utilizationModel);
		c.setClassType(classId);
		c.setUserId(broker.getId());
		c.setExecStartTime(arrivalTime.doubleValue());
		c.setActualSubmitTime(arrivalTime.doubleValue());

		return c;
	}


	private void cloudSimInit(int hostNumber) {
		CloudSim.init(1, Calendar.getInstance(), false);
		this.createDatacenter(hostNumber);	
	}



	/**
	 * Creates the datacenter.
	 *
	 * @param hostNumber the number of desired host
	 * in datacenter
	 *
	 * @return the datacenter
	 */
	private Datacenter createDatacenter(int hostNumber) {

		List<Host> hostList = new ArrayList<Host>();

		int hostId = 0;
		for (int i = 0; i < hostNumber; i++) {
			List<Pe> peList = new ArrayList<Pe>();
			peList.add(new Pe(i, new PeProvisionerSimple(Globals.PE_MIPS)));

			hostList.add(new Host(hostId+i, new RamProvisionerSimple(Globals.HOST_RAM),
					new BwProvisionerSimple(Globals.HOST_BW), Globals.HOST_STORAGE, peList,
					new VmSchedulerSpaceShared(peList))); 
		}

		System.out.println("Host array size: " + hostList.size());

		LinkedList<Storage> storageList = new LinkedList<Storage>(); 

		DatacenterCharacteristics characteristics = new DatacenterCharacteristics(
				Globals.DATACENTER_ARCH, Globals.DATACENTER_OS, Globals.DATACENTER_VMM, hostList, Globals.DATACENTER_TIME_ZONE, 
				Globals.DATACENTER_COST_PER_CPU, Globals.DATACENTER_COST_PER_MEM, Globals.DATACENTER_COST_PER_STORAGE, Globals.DATACENTER_COST_PER_BW);

		Datacenter datacenter = null;
		try {
			datacenter = new Datacenter(Globals.DATACENTER_NAME, characteristics, new VmAllocationPolicySimple(hostList), storageList, 0);
			System.out.println(Globals.DATACENTER_NAME + " has id: " + datacenter.getId());
		} catch (Exception e) {
			e.printStackTrace();
			System.out.println("error in creating datacenter: aborting");
			System.exit(-1);
		}

		return datacenter;
	}





	public boolean SLAsIsMet() {
		return SLAsIsMet;
	}


	public void printStats() {
		broker.printStatistics();
		//printCloudletList(cloudletList);
	}

	public void start(){

		CloudSim.startSimulation();
		CloudSim.stopSimulation();
		this.SLAsIsMet = broker.SLAsAreMet();

	}
	
	public void startWithParams(long seed, int numberOfServer) throws Exception{

		this.seed = seed;
		this.m = numberOfServer;
		Itecs.mu = 1/Globals.CLOUDLET_LENGTH_MEAN;
		
		System.out.println("\nINITIALIZING ITECS WITH " + this.m + " HOSTS and " + this.seed + " as SEED");
		//Itecs.writer = new FileWriter("output/output-" + brokerType + "-" + m + "-" + seed + "-" + lambda1 + "-" + lambda2+ "-" + mu/Globals.VM_MIPS  + ".csv", false);

		vmList.clear();
		cloudletList.clear();
		cloudSimInit(m);

		if (brokerType.equals(Globals.FCFS_BROKER)){
			broker = new FCFSBroker("FCFSbroker", Globals.SIMULATION_TIME,  sla1, sla2);
		}
		else if (brokerType.equals(Globals.HOL_BROKER)){
			broker = new HOLBroker("HOLbroker", Globals.SIMULATION_TIME, sla1, sla2);
		}
		else if (brokerType.equals(Globals.PDP_BROKER)){
			broker = new PDPBroker("PDPbroker", Globals.SIMULATION_TIME, sla1, sla2);
		}
		else {
			broker = new FCFSBroker("FCFSbroker", Globals.SIMULATION_TIME, sla1, sla2);
			System.out.println("Default Broker: FCFSBroker");
		}

		ArrayList<Double> serviceTimes1 = null;
		ArrayList<Double> serviceTimes2 = null;

		ArrayList<Double> arrivalTimes1 = MathUtils.getArrivals(this.generateSeed(), lambda1);
		ArrayList<Double> arrivalTimes2 = MathUtils.getArrivals(this.generateSeed(), lambda2);

		if (serviceDistribution.equals("EXP")) 
		{
			serviceTimes1 = MathUtils.getExponentialServiceTimes(this.generateSeed(), mu, arrivalTimes1.size());
			serviceTimes2 = MathUtils.getExponentialServiceTimes(this.generateSeed(), mu, arrivalTimes2.size());
		}
		else if (serviceDistribution.equals("PARETO")) 
		{
			serviceTimes1 = MathUtils.getParetoServiceTimes(this.generateSeed(), Globals.PARETO_SHAPE, Globals.PARETO_LOCATION, arrivalTimes1.size());
			serviceTimes2 = MathUtils.getParetoServiceTimes(this.generateSeed(), Globals.PARETO_SHAPE, Globals.PARETO_LOCATION, arrivalTimes2.size());
		}
		else 
		{
			serviceTimes1 = MathUtils.getExponentialServiceTimes(this.generateSeed(), mu, arrivalTimes1.size());
			serviceTimes2 = MathUtils.getExponentialServiceTimes(this.generateSeed(), mu, arrivalTimes2.size());
			System.out.println("Default service time distribution: EXPONENTIAL");
		}

		vmList = getVms(broker.getId(), m);
		cloudletList = getCloudlets(arrivalTimes1, arrivalTimes2, serviceTimes1, serviceTimes2);

		broker.submitVmList(vmList);
		broker.submitCloudletList(cloudletList);
		
		CloudSim.startSimulation();
		CloudSim.stopSimulation();
		
		this.SLAsIsMet = broker.SLAsAreMet();

	}

//	public static void write (String str){
//		try {
//			writer.write(str + "\n");
//			writer.flush();
//		} catch (IOException e) {
//			e.printStackTrace();
//		}
//	}



	public static int getM(String type, int startingM, SLA sla1, SLA sla2, double lambda1, double lambda2, double mu, long seed, String serviceDistribution) throws Exception{

		int m = startingM;

	
			Itecs i = new Itecs(type, sla1, sla2, serviceDistribution, lambda1, lambda2);

			while (i.isMetable()) {	
				i = new Itecs(type, sla1, sla2, serviceDistribution, lambda1, lambda2);
				i.startWithParams(seed, m);
				i.printStats();
				m++;
			}
		
		if (i.SLAsIsMet()){
			return m;
		}
		else {
			return -1;
		}
		
		
		
	}

	private boolean isMetable() {
		if (broker==null){
			return true;
		}
		return !SLAsIsMet && broker.getMaxQueueSize()!=0;
	}

	
	public double getUtilization(){
		return broker.getUtilization();
	}


	
	
	public static int getOptimalM(String type, SLA sla1, SLA sla2, double lambda1, double lambda2, String serviceDistribution) throws Exception {
		int mcorrect = 0;
		int seedIndex = 0;
		int m = Globals.SHARED_STARTING_M;
		
		while (mcorrect!=3 && seedIndex < Globals.SEEDS.length){
			m = Itecs.getM(type, m, sla1, sla2, lambda1, lambda2, 1/Globals.CLOUDLET_LENGTH_MEAN,Globals.SEEDS[seedIndex], serviceDistribution);
			
			if(m==-1)
				return -1;
			
			Itecs i = new Itecs(type, sla1, sla2, serviceDistribution, lambda1, lambda2);
			mcorrect++;
			int nextseedIndex=(seedIndex +1)%Globals.SEEDS.length;
			for (int j=0; j<3; j++){
				i.startWithParams(Globals.SEEDS[(nextseedIndex+j)%Globals.SEEDS.length], m);
				i.printStats();
				if (i.SLAsIsMet()){
					mcorrect++;
					if (mcorrect==3){
						break;
					}
				}
				else {
					mcorrect = 0;
					seedIndex = (nextseedIndex+j)%Globals.SEEDS.length; 
					break;
				}
			}
		}
		System.out.println("Finally m is: " + m);
		return m;
	}


	public long generateSeed()						
	{
		long a=1220703125; //moltiplicatore a=5^13
		Long m=new Long("2147483648"); //modulo m=2^31
		for(int i=0;i<20;i++)
			seed=(long)((a*seed)%(m.doubleValue()));		
		return seed;
	}


	public long getSeed() {
		return seed;
	}


	public void setSeed(long seed) {
		this.seed = seed;
	}

	
	public double getRunThroughput() {
		return broker.getRunThroughput();
	}

	
	public ArrayList<Observation> getObservations() {
		return broker.getObservations();
	}
	
	
	@SuppressWarnings("unused")
	private static void printCloudletList(ArrayList<ExtendedCloudlet> cloudletList2) {
		int size = cloudletList2.size();
		Cloudlet cloudlet;

		String indent = "    ";
		String shortindent = "  ";
		Log.printLine();
		Log.printLine("========== OUTPUT ========== ( size: " + size + " cloudlets)");
		Log.printLine("Cloudlet ID" + indent + "STATUS" + indent
				+ "Data center ID" + indent + "VM ID" + indent + "Time" + indent
				+ "Start Time" + indent + "Finish Time");

		DecimalFormat dft = new DecimalFormat("####.00");
		for (int i = 0; i < size; i++) {

			cloudlet = cloudletList2.get(i);

			if(!cloudlet.isFinished()){
				continue;
			}

			int dynsize = new Integer(cloudlet.getCloudletId()).toString().length();
			String dynintent = "";
			for(int j = 0; j < 12 - dynsize; j++)
				dynintent += " ";

			Log.print(shortindent + cloudlet.getCloudletId() + dynintent);

			if(cloudlet.isFinished()){
				if (cloudlet.getCloudletStatus() == Cloudlet.SUCCESS) {
					Log.print("SUCCESS");
				}
			}
			else{
				System.out.println("DEATH, status: " + cloudlet.getCloudletStatus());
				continue;
			}

			Log.printLine(indent + indent + cloudlet.getResourceId()
					+ indent + indent + indent + cloudlet.getVmId()
					+ indent + indent
					+ dft.format(cloudlet.getActualCPUTime()) + indent
					+ indent + dft.format(cloudlet.getExecStartTime())
					+ indent + indent
					+ dft.format(cloudlet.getFinishTime()));
		}

		Log.printLine("======== OUTPUT END ======== ( size: " + size + " cloudlets)");

	}
	
}
